查看原文
其他

从源码看Flutter BuildContext的秘密

徐宜生 群英传
2024-08-24

点击上方蓝字关注我,知识会给你力量


我们每次在写Flutter代码的时候,都会看到这个参数——BuildContext,在Android开发中,也经常看见一个类似的东西——Context,它们是不是一样的呢?其实说一样也对,它们都是上下文的关键承载者,但是却也不一样,因为它们本质上是两个不同的概念。在Flutter中,BuildContext的源码如下。

从注释中我们就可以看出,[BuildContext]对象实际上是[Element]对象。[BuildContext]接口是用来阻止对[Element]对象的直接操作,它就是为了避免直接操纵Element类而创建的。

Element是Flutter UI中的一个非常重要的组成,Flutter UI在创建时,会通过Widget的createElement方法创建Element,然后Framework会调用Element实例的mount方法,在这个方法中,根据需要创建RenderObject,并挂载到Element的renderObject属性上,实际的布局和绘制,通常都是通过RenderObject的实现类RenderBox来实现的。

所以,我们甚至可以直接把Context强转为Element,从而调用Element的方法,例如下面的代码。

var size = ((context as Element).findRenderObject() as RenderBox).size;

(context as Element).markNeedsBuild();

在使用BuildContext的时候,我们最常见的一个误区就是下面这个例子。

这段代码很简单,就是在当前页面上路由到一个新的页面,但是我们执行后,上面的代码会报错。

从错误原因上我们可以看到,就是Context的问题,也就是of(context)这个方法。类似的代码风格,我们在Flutter中可以找到很多,例如下面这些。

Navigator.of(context)
Scaffold.of(context).openDrawer()
Theme.of(context).copyWith()
……

就以Navigator.of(context)为例,我们来看下of方法的实现。

可以看到,关键代码就是通过context.findRootAncestorStateOfType()或者是context.findAncestorStateOfType()方法,向上遍历Element tree,并找到最近匹配的NavigatorState。也就是说of实际上是对context跨组件获取数据的一个封装方法。

而我们的Navigator的push操作就是通过找到的NavigatorState来完成的。

那么我们现在来看下上面的那个错误具体是怎么产生的。当我们在build函数中使用Navigator.of(context)的时候,这个context实际上是通过MyApp这个widget创建出来的Element对象,而of方法向上寻找祖先节点的时候(MyApp的祖先节点),其实并不存在MaterialApp,也就没有它所提供的Navigator,所以就出错了。

那么当我们把Scaffold的部分拆成另外一个widget的时候,我们在FirstPage的build函数中,获取的就是FirstPage的BuildContext,然后向上寻找发现了MaterialApp,并找到它提供的Navigator,于是就可以愉快进行页面跳转了。所以要解决这个问题,一般有两个方法,一个就是抽取出一个新的Widget组件,或者是通过Builder组件,为后续Widget创建一个新的BuildContext环境。

所以,看到这里,你可以认为,BuildContext,实际上就是当前Widget在Element树上的句柄。

下面我们就从源码角度,来看下BuildContext的创建与加载的过程。

我们以StatelessWidget为例,在创建StatelessWidget的时候,首先会去createElement,并将当前widget传给Element,即StatelessElement。

在这个Element中,我们发现它的build方法,实际上就是调用了Widget的build方法,同时,传入了this,这个this,实际上就是我们在Widget的build方法中看到的BuildContext,到处,我们终于理清了,为什么BuildContext就是Element了。

同时,正是由于Flutter视图的树形结构,可以让我们很方便的在树上游走,这就需要我们用到它的一些游走的方法。

dependOnInheritedElementInheritedWidgetworking on the base of ancestor's widget and rebuilding when ancestors change
dependOnInheritedWidgetOfExactTypeT?runtime type of widgets or functions or model
describeElementDiagnosticsNodedescriptions of elements
describeMissingAncestorListList of missing ancestors
describeOwnershipChainDiagnosticsNodedescribes the ownership chain to the error report
describeWidgetDiagnosticsNodedescribe the details and working features of the widget
dispatchNotificationvoidbubble notification indicator at the context
findAncestorRenderObjectOfTypeT?runtime type of RenderObjectOfType
findAncestorStateOfTypeT?runtime type of StateOfType
findAncestorWidgetOfExactTypeT?runtime type of WidgetOfExactType
findRenderObjectT?current render object which is created by itSelf
findRootAncestorStateOfTypeT?runtime type ancestor's of given T type stateful widgets instance
getElementForInheritedWidgetOfExactTypeInheritedElement?Type of concrete inheritedWidget subclass
noSuchMethoddynamicdynamic type of method
toStringstringconverting the date to string through .toString methods provided by the framework
visitAncestorElementsvoidworks for the call back return function, call back can not be null
visitChildElementsvoidchildren of the widgets or visitor

这些方法也不用死记硬背,你只需要时刻记得「那几棵树」即可。

向大家推荐下我的网站 https://www.yuque.com/xuyisheng  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


继续滑动看下一个
群英传
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存